<!doctype html>
<html lang="zh">
<head>
  <meta charset="utf-8" />
  <title>工作流 Demo（Flask + SSE）</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
      margin: 20px;
      line-height: 1.6;
    }
    h2 { margin-bottom: 0; }
    .toolbar { margin: 12px 0; }
    input[type=text] { width: 280px; }
    button { margin-right: 6px; }
    .container {
      display: flex;
      gap: 20px;
      margin-top: 16px;
    }
    .left, .right {
      flex: 1;
      min-width: 300px;
    }
    .panel {
      border: 1px solid #ddd;
      background: #fafafa;
      padding: 10px 14px;
      border-radius: 4px;
      overflow: auto;
    }
    #workflowJson { height: 220px; white-space: pre-wrap; }
    #log { height: 420px; white-space: pre-wrap; }
    .ok { color: #0a7; }
    .err { color: #c00; }
  </style>
</head>
<body>
  <h2>工作流 Demo（Flask + SSE）</h2>
  <p>一个最小的异步工作流演示（start → code → end），通过 SSE 实时监听执行过程。</p>

  <div class="toolbar">
    <button id="btnStart">启动最小工作流</button>
    <span>exec_id：<input type="text" id="execId" placeholder="自动填入" /></span>
    <button id="btnCancel">取消</button>
    <button id="btnPause">暂停</button>
    <button id="btnResume">恢复</button>
    <button id="btnStatus">查询状态</button>
  </div>

  <div class="container">
    <div class="left">
      <h3>工作流结构</h3>
      <div id="workflowJson" class="panel"></div>

      <h4>节点说明</h4>
      <ul>
        <li><b>start_0：</b> 起点节点，输出变量 <code>x=1</code>。</li>
        <li><b>code_1：</b> 代码节点（异步 Python）：
          <pre><code>async def main(args):
    import asyncio
    await asyncio.sleep(3)
    p = args.params
    return {"result": p.get("x", 0) + 1}</code></pre>
        </li>
        <li><b>end_0：</b> 汇总最终结果。</li>
      </ul>
    </div>

    <div class="right">
      <h3>事件日志</h3>
      <div id="log" class="panel"></div>
    </div>
  </div>

  <script>
    const BASE = location.origin;
    const $ = (id) => document.getElementById(id);
    let es = null;

    // === Demo 工作流结构 ===
    const workflowDemo = {
      "nodes": [
        { "id": "start_0", "type": "start", "data": { "outputs": { "x": 1 } } },
        { "id": "code_1", "type": "code", "data": {
            "codes": { "type": "python", "value": "async def main(args):\\n    import asyncio\\n    await asyncio.sleep(3)\\n    p = args.params\\n    return {\\\"result\\\": p.get(\\\"x\\\", 0) + 1}" }
          }
        },
        { "id": "end_0", "type": "end", "data": {} }
      ],
      "edges": [
        { "sourceNodeID": "start_0", "targetNodeID": "code_1" },
        { "sourceNodeID": "code_1", "targetNodeID": "end_0" }
      ]
    };
    $("workflowJson").textContent = JSON.stringify(workflowDemo, null, 2);

    function log(line, cls = "") {
      const box = $("log");
      const t = new Date().toLocaleTimeString();
      box.innerHTML += (cls ? `<span class="${cls}">` : "") + `[${t}] ${line}` + (cls ? "</span>" : "") + "\n";
      box.scrollTop = box.scrollHeight;
    }

    // === 自动订阅 SSE ===
    function subscribeSSE(execId) {
      if (!execId) return alert("exec_id 为空！");
      if (es) es.close();

      const url = `${BASE}/executions/${execId}/subscribe`;
      es = new EventSource(url);

      es.addEventListener("connected", e => log(`connected: ${e.data}`, "ok"));
      es.addEventListener("exec_started", e => log(`exec_started: ${e.data}`));
      es.addEventListener("node_started", e => log(`node_started: ${e.data}`));
      es.addEventListener("node_finished", e => log(`node_finished: ${e.data}`, "ok"));
      es.addEventListener("node_failed", e => log(`node_failed: ${e.data}`, "err"));
      es.addEventListener("exec_finished", e => {
        log(`exec_finished: ${e.data}`, "ok");
        es.close(); // 执行完自动断开
      });
      es.onerror = () => log("SSE 连接异常或断开", "err");

      log(`SSE 已连接：${url}`);
    }

    // === 启动工作流（自动订阅）===
    $("btnStart").onclick = async () => {
      try {
        const r = await fetch(`${BASE}/demo/start`, { method: "POST" });
        const j = await r.json();
        if (!j.success) throw new Error(j.message);
        const execId = j.result.exec_id;
        $("execId").value = execId;
        log(`启动成功 exec_id=${execId}`, "ok");
        subscribeSSE(execId);  // ✅ 自动订阅
      } catch (e) {
        log(`启动失败：${e}`, "err");
      }
    };

    // === 其他控制 ===
    $("btnStatus").onclick = async () => {
      const execId = $("execId").value.trim();
      if (!execId) return alert("请先填写 exec_id");
      const r = await fetch(`${BASE}/executions/${execId}/status`);
      const j = await r.json();
      log(`状态：${JSON.stringify(j.result || j)}`);
    };

    $("btnCancel").onclick = async () => {
      const execId = $("execId").value.trim();
      if (!execId) return alert("请先填写 exec_id");
      const r = await fetch(`${BASE}/executions/${execId}/cancel`, { method: "POST" });
      const j = await r.json();
      log(`取消：${j.success ? "OK" : j.message}`, j.success ? "ok" : "err");
    };

    $("btnPause").onclick = async () => {
      const execId = $("execId").value.trim();
      if (!execId) return alert("请先填写 exec_id");
      const r = await fetch(`${BASE}/executions/${execId}/pause`, { method: "POST" });
      const j = await r.json();
      log(`暂停：${j.success ? "OK" : j.message}`, j.success ? "ok" : "err");
    };

    $("btnResume").onclick = async () => {
      const execId = $("execId").value.trim();
      if (!execId) return alert("请先填写 exec_id");
      const r = await fetch(`${BASE}/executions/${execId}/resume`, { method: "POST" });
      const j = await r.json();
      log(`恢复：${j.success ? "OK" : j.message}`, j.success ? "ok" : "err");
    };
  </script>
</body>
</html>
